<?php

/**
 * JPGraph v4.0.3
 */

namespace lemon\JpGraph\Graph;

use lemon\JpGraph\Image;
use lemon\JpGraph\Plot;
use lemon\JpGraph\Text;
use lemon\JpGraph\Util;

/**
 * @class GanttGraph
 * // Description: Main class to handle gantt graphs
 */
class GanttGraph extends Graph
{
	public $scale; // Public accessible
	public $hgrid;
	private $iObj					= []; // Gantt objects
	private $iLabelHMarginFactor	 = 0.2; // 10% margin on each side of the labels
	private $iLabelVMarginFactor	 = 0.4; // 40% margin on top and bottom of label
	private $iLayout				 = GANTT_FROMTOP; // Could also be GANTT_EVEN
	private $iSimpleFont			 = FF_FONT1;
	private $iSimpleFontSize		 = 11;
	private $iSimpleStyle			= GANTT_RDIAG;
	private $iSimpleColor			= 'yellow';
	private $iSimpleBkgColor		 = 'red';
	private $iSimpleProgressBkgColor = 'gray';
	private $iSimpleProgressColor	= 'darkgreen';
	private $iSimpleProgressStyle	= GANTT_SOLID;
	private $iZoomFactor			 = 1.0;

	/**
	 * CONSTRUCTOR
	 * // Create a new gantt graph.
	 *
	 * @param mixed $aWidth
	 * @param mixed $aHeight
	 * @param mixed $aCachedName
	 * @param mixed $aTimeOut
	 * @param mixed $aInline
	 */
	public function __construct($aWidth = 0, $aHeight = 0, $aCachedName = '', $aTimeOut = 0, $aInline = true){
		// Backward compatibility
		if ($aWidth == -1) {
			$aWidth = 0;
		}

		if ($aHeight == -1) {
			$aHeight = 0;
		}

		if ($aWidth < 0 || $aHeight < 0) {
			Util\JpGraphError::RaiseL(6002);
			//("You can't specify negative sizes for Gantt graph dimensions. Use 0 to indicate that you want the library to automatically determine a dimension.");
		}
		parent::__construct($aWidth, $aHeight, $aCachedName, $aTimeOut, $aInline);
		$this->scale = new GanttScale($this->img);

		// Default margins
		$this->img->SetMargin(15, 17, 25, 15);

		$this->hgrid = new HorizontalGridLine();

		$this->scale->ShowHeaders(GANTT_HWEEK | GANTT_HDAY);
		$this->SetBox();
	}

	/**
	 * PUBLIC METHODS.
	 *
	 * @param mixed $aFont
	 * @param mixed $aSize
	 */
	public function SetSimpleFont($aFont, $aSize){
		$this->iSimpleFont	 = $aFont;
		$this->iSimpleFontSize = $aSize;
	}

	public function SetSimpleStyle($aBand, $aColor, $aBkgColor){
		$this->iSimpleStyle	= $aBand;
		$this->iSimpleColor	= $aColor;
		$this->iSimpleBkgColor = $aBkgColor;
	}

	// A utility function to help create basic Gantt charts
	public function CreateSimple($data, $constrains = [], $progress = []){
		$num = safe_count($data);
		for ($i = 0; $i < $num; ++$i) {
			switch ($data[$i][1]) {
				case ACTYPE_GROUP:
					// Create a slightly smaller height bar since the
					// "wings" at the end will make it look taller
					$a = new Plot\GanttBar($data[$i][0], $data[$i][2], $data[$i][3], $data[$i][4], '', 8);
					$a->title->SetFont($this->iSimpleFont, FS_BOLD, $this->iSimpleFontSize);
					$a->rightMark->Show();
					$a->rightMark->SetType(MARK_RIGHTTRIANGLE);
					$a->rightMark->SetWidth(8);
					$a->rightMark->SetColor('black');
					$a->rightMark->SetFillColor('black');

					$a->leftMark->Show();
					$a->leftMark->SetType(MARK_LEFTTRIANGLE);
					$a->leftMark->SetWidth(8);
					$a->leftMark->SetColor('black');
					$a->leftMark->SetFillColor('black');

					$a->SetPattern(BAND_SOLID, 'black');
					$csimpos = 6;

					break;
				case ACTYPE_NORMAL:
					$a = new Plot\GanttBar($data[$i][0], $data[$i][2], $data[$i][3], $data[$i][4], '', 10);
					$a->title->SetFont($this->iSimpleFont, FS_NORMAL, $this->iSimpleFontSize);
					$a->SetPattern($this->iSimpleStyle, $this->iSimpleColor);
					$a->SetFillColor($this->iSimpleBkgColor);
					// Check if this activity should have a constrain line
					$n = safe_count($constrains);
					for ($j = 0; $j < $n; ++$j) {
						if (empty($constrains[$j]) || (safe_count($constrains[$j]) != 3)) {
							Util\JpGraphError::RaiseL(6003, $j);
							//("Invalid format for Constrain parameter at index=$j in CreateSimple(). Parameter must start with index 0 and contain arrays of (Row,Constrain-To,Constrain-Type)");
						}
						if ($constrains[$j][0] == $data[$i][0]) {
							$a->SetConstrain($constrains[$j][1], $constrains[$j][2], 'black', ARROW_S2, ARROWT_SOLID);
						}
					}

					// Check if this activity have a progress bar
					$n = safe_count($progress);
					for ($j = 0; $j < $n; ++$j) {
						if (empty($progress[$j]) || (safe_count($progress[$j]) != 2)) {
							Util\JpGraphError::RaiseL(6004, $j);
							//("Invalid format for Progress parameter at index=$j in CreateSimple(). Parameter must start with index 0 and contain arrays of (Row,Progress)");
						}
						if ($progress[$j][0] == $data[$i][0]) {
							$a->progress->Set($progress[$j][1]);
							$a->progress->SetPattern(
								$this->iSimpleProgressStyle,
								$this->iSimpleProgressColor
							);
							$a->progress->SetFillColor($this->iSimpleProgressBkgColor);
							//$a->progress->SetPattern($progress[$j][2],$progress[$j][3]);
							break;
						}
					}
					$csimpos = 6;

					break;
				case ACTYPE_MILESTONE:
					$a = new Plot\MileStone($data[$i][0], $data[$i][2], $data[$i][3]);
					$a->title->SetFont($this->iSimpleFont, FS_NORMAL, $this->iSimpleFontSize);
					$a->caption->SetFont($this->iSimpleFont, FS_NORMAL, $this->iSimpleFontSize);
					$csimpos = 5;

					break;
				default:
					die('Unknown activity type');

					break;
			}

			// Setup caption
			$a->caption->Set($data[$i][$csimpos - 1]);

			// Check if this activity should have a CSIM target�?
			if (!empty($data[$i][$csimpos])) {
				$a->SetCSIMTarget($data[$i][$csimpos]);
				$a->SetCSIMAlt($data[$i][$csimpos + 1]);
			}
			if (!empty($data[$i][$csimpos + 2])) {
				$a->title->SetCSIMTarget($data[$i][$csimpos + 2]);
				$a->title->SetCSIMAlt($data[$i][$csimpos + 3]);
			}

			$this->Add($a);
		}
	}

	// Set user specified scale zoom factor when auto sizing is used
	public function SetZoomFactor($aZoom){
		$this->iZoomFactor = $aZoom;
	}

	// Set what headers should be shown
	public function ShowHeaders($aFlg){
		$this->scale->ShowHeaders($aFlg);
	}

	// Specify the fraction of the font height that should be added
	// as vertical margin
	public function SetLabelVMarginFactor($aVal){
		$this->iLabelVMarginFactor = $aVal;
	}

	// Synonym to the method above
	public function SetVMarginFactor($aVal){
		$this->iLabelVMarginFactor = $aVal;
	}

	// Add a new Gantt object
	public function Add($aObject){
		if (is_array($aObject) && safe_count($aObject) > 0) {
			$cl = $aObject[0];
			if (($cl instanceof Plot\IconPlot)) {
				$this->AddIcon($aObject);
			} elseif (($cl instanceof Text\Text)) {
				$this->AddText($aObject);
			} else {
				$n = safe_count($aObject);
				for ($i = 0; $i < $n; ++$i) {
					$this->iObj[] = $aObject[$i];
				}
			}
		} else {
			if (($aObject instanceof Plot\IconPlot)) {
				$this->AddIcon($aObject);
			} elseif (($aObject instanceof Text\Text)) {
				$this->AddText($aObject);
			} else {
				$this->iObj[] = $aObject;
			}
		}
	}

	public function StrokeTexts(){
		// Stroke any user added text objects
		if ($this->texts != null) {
			$n = safe_count($this->texts);
			for ($i = 0; $i < $n; ++$i) {
				if ($this->texts[$i]->iScalePosX !== null && $this->texts[$i]->iScalePosY !== null) {
					$x = $this->scale->TranslateDate($this->texts[$i]->iScalePosX);
					$y = $this->scale->TranslateVertPos($this->texts[$i]->iScalePosY);
					$y -= $this->scale->GetVertSpacing() / 2;
				} else {
					$x = $y = null;
				}
				$this->texts[$i]->Stroke($this->img, $x, $y);
			}
		}
	}

	// Override inherit method from Graph and give a warning message
	public function SetScale($aAxisType, $aYMin = 1, $aYMax = 1, $aXMin = 1, $aXMax = 1){
		Util\JpGraphError::RaiseL(6005);
		//("SetScale() is not meaningfull with Gantt charts.");
	}

	// Specify the date range for Gantt graphs (if this is not set it will be
	// automtically determined from the input data)
	public function SetDateRange($aStart, $aEnd){
		// Adjust the start and end so that the indicate the
		// begining and end of respective start and end days
		if (strpos($aStart, ':') === false) {
			$aStart = date('Y-m-d 00:00', strtotime($aStart));
		}

		if (strpos($aEnd, ':') === false) {
			$aEnd = date('Y-m-d 23:59', strtotime($aEnd));
		}

		$this->scale->SetRange($aStart, $aEnd);
	}

	// Get the maximum width of the activity titles columns for the bars
	// The name is lightly misleading since we from now on can have
	// multiple columns in the label section. When this was first written
	// it only supported a single label, hence the name.
	public function GetMaxLabelWidth(){
		$m = 10;
		if ($this->iObj != null) {
			$marg = $this->scale->actinfo->iLeftColMargin + $this->scale->actinfo->iRightColMargin;
			$n	= safe_count($this->iObj);
			for ($i = 0; $i < $n; ++$i) {
				if (!empty($this->iObj[$i]->title)) {
					if ($this->iObj[$i]->title->HasTabs()) {
						list($tot, $w) = $this->iObj[$i]->title->GetWidth($this->img, true);
						$m			 = max($m, $tot);
					} else {
						$m = max($m, $this->iObj[$i]->title->GetWidth($this->img));
					}
				}
			}
		}

		return $m;
	}

	// Get the maximum height of the titles for the bars
	public function GetMaxLabelHeight(){
		$m = 10;
		if ($this->iObj != null) {
			$n = safe_count($this->iObj);
			// We can not include the title of GnttVLine since that title is stroked at the bottom
			// of the Gantt bar and not in the activity title columns
			for ($i = 0; $i < $n; ++$i) {
				if (!empty($this->iObj[$i]->title) && !($this->iObj[$i] instanceof Plot\GanttVLine)) {
					$m = max($m, $this->iObj[$i]->title->GetHeight($this->img));
				}
			}
		}

		return $m;
	}

	public function GetMaxBarAbsHeight(){
		$m = 0;
		if ($this->iObj != null) {
			$m = $this->iObj[0]->GetAbsHeight($this->img);
			$n = safe_count($this->iObj);
			for ($i = 1; $i < $n; ++$i) {
				$m = max($m, $this->iObj[$i]->GetAbsHeight($this->img));
			}
		}

		return $m;
	}

	// Get the maximum used line number (vertical position) for bars
	public function GetBarMaxLineNumber(){
		$m = 1;
		if ($this->iObj != null) {
			$m = $this->iObj[0]->GetLineNbr();
			$n = safe_count($this->iObj);
			for ($i = 1; $i < $n; ++$i) {
				$m = max($m, $this->iObj[$i]->GetLineNbr());
			}
		}

		return $m;
	}

	// Get the minumum and maximum used dates for all bars
	public function GetBarMinMax(){
		$start = 0;
		$n	 = safe_count($this->iObj);
		while ($start < $n && $this->iObj[$start]->GetMaxDate() === false) {
			++$start;
		}

		if ($start >= $n) {
			Util\JpGraphError::RaiseL(6006);
			//('Cannot autoscale Gantt chart. No dated activities exist. [GetBarMinMax() start >= n]');
		}

		$max = $this->scale->NormalizeDate($this->iObj[$start]->GetMaxDate());
		$min = $this->scale->NormalizeDate($this->iObj[$start]->GetMinDate());

		for ($i = $start + 1; $i < $n; ++$i) {
			$rmax = $this->scale->NormalizeDate($this->iObj[$i]->GetMaxDate());
			if ($rmax != false) {
				$max = max($max, $rmax);
			}

			$rmin = $this->scale->NormalizeDate($this->iObj[$i]->GetMinDate());
			if ($rmin != false) {
				$min = min($min, $rmin);
			}
		}
		$minDate = date('Y-m-d', $min);
		$min	 = strtotime($minDate);
		$maxDate = date('Y-m-d 23:59', $max);
		$max	 = strtotime($maxDate);

		return [$min, $max];
	}

	// Create a new auto sized canvas if the user hasn't specified a size
	// The size is determined by what scale the user has choosen and hence
	// the minimum width needed to display the headers. Some margins are
	// also added to make it better looking.
	public function AutoSize(){
		if ($this->img->img == null) {
			// The predefined left, right, top, bottom margins.
			// Note that the top margin might incease depending on
			// the title.
			$hadj = $vadj = 0;
			if ($this->doshadow) {
				$hadj = $this->shadow_width;
				$vadj = $this->shadow_width + 5;
			}

			$lm = $this->img->left_margin;
			$rm = $this->img->right_margin + $hadj;
			$rm += 2;
			$tm = $this->img->top_margin;
			$bm = $this->img->bottom_margin + $vadj;
			$bm += 2;

			// If there are any added Plot\GanttVLine we must make sure that the
			// bottom margin is wide enough to hold a title.
			$n = safe_count($this->iObj);
			for ($i = 0; $i < $n; ++$i) {
				if ($this->iObj[$i] instanceof Plot\GanttVLine) {
					$bm = max($bm, $this->iObj[$i]->title->GetHeight($this->img) + 10);
				}
			}

			// First find out the height
			$n	  = $this->GetBarMaxLineNumber() + 1;
			$m	  = max($this->GetMaxLabelHeight(), $this->GetMaxBarAbsHeight());
			$height = $n * ((1 + $this->iLabelVMarginFactor) * $m);

			// Add the height of the scale titles
			$h = $this->scale->GetHeaderHeight();
			$height += $h;

			// Calculate the top margin needed for title and subtitle
			if ($this->title->t != '') {
				$tm += $this->title->GetFontHeight($this->img);
			}
			if ($this->subtitle->t != '') {
				$tm += $this->subtitle->GetFontHeight($this->img);
			}

			// ...and then take the bottom and top plot margins into account
			$height += $tm + $bm + $this->scale->iTopPlotMargin + $this->scale->iBottomPlotMargin;
			// Now find the minimum width for the chart required

			// If day scale or smaller is shown then we use the day font width
			// as the base size unit.
			// If only weeks or above is displayed we use a modified unit to
			// get a smaller image.
			if ($this->scale->IsDisplayHour() || $this->scale->IsDisplayMinute()) {
				// Add 2 pixel margin on each side
				$fw = $this->scale->day->GetFontWidth($this->img) + 4;
			} elseif ($this->scale->IsDisplayWeek()) {
				$fw = 8;
			} elseif ($this->scale->IsDisplayMonth()) {
				$fw = 4;
			} else {
				$fw = 2;
			}

			$nd = $this->scale->GetNumberOfDays();

			if ($this->scale->IsDisplayDay()) {
				// If the days are displayed we also need to figure out
				// how much space each day's title will require.
				switch ($this->scale->day->iStyle) {
					case DAYSTYLE_LONG:
						$txt = 'Monday';

						break;
					case DAYSTYLE_LONGDAYDATE1:
						$txt = 'Monday 23 Jun';

						break;
					case DAYSTYLE_LONGDAYDATE2:
						$txt = 'Monday 23 Jun 2003';

						break;
					case DAYSTYLE_SHORT:
						$txt = 'Mon';

						break;
					case DAYSTYLE_SHORTDAYDATE1:
						$txt = 'Mon 23/6';

						break;
					case DAYSTYLE_SHORTDAYDATE2:
						$txt = 'Mon 23 Jun';

						break;
					case DAYSTYLE_SHORTDAYDATE3:
						$txt = 'Mon 23';

						break;
					case DAYSTYLE_SHORTDATE1:
						$txt = '23/6';

						break;
					case DAYSTYLE_SHORTDATE2:
						$txt = '23 Jun';

						break;
					case DAYSTYLE_SHORTDATE3:
						$txt = 'Mon 23';

						break;
					case DAYSTYLE_SHORTDATE4:
						$txt = '88';

						break;
					case DAYSTYLE_CUSTOM:
						$txt = date($this->scale->day->iLabelFormStr, strtotime('2003-12-20 18:00'));

						break;
					case DAYSTYLE_ONELETTER:
					default:
						$txt = 'M';

						break;
				}
				$fw = $this->scale->day->GetStrWidth($this->img, $txt) + 6;
			}

			// If we have hours enabled we must make sure that each day has enough
			// space to fit the number of hours to be displayed.
			if ($this->scale->IsDisplayHour()) {
				// Depending on what format the user has choose we need different amount
				// of space. We therefore create a typical string for the choosen format
				// and determine the length of that string.
				switch ($this->scale->hour->iStyle) {
					case HOURSTYLE_HMAMPM:
						$txt = '12:00pm';

						break;
					case HOURSTYLE_H24:
						// 13
						$txt = '24';

						break;
					case HOURSTYLE_HAMPM:
						$txt = '12pm';

						break;
					case HOURSTYLE_CUSTOM:
						$txt = date($this->scale->hour->iLabelFormStr, strtotime('2003-12-20 18:00'));

						break;
					case HOURSTYLE_HM24:
					default:
						$txt = '24:00';

						break;
				}

				$hfw = $this->scale->hour->GetStrWidth($this->img, $txt) + 6;
				$mw  = $hfw;
				if ($this->scale->IsDisplayMinute()) {
					// Depending on what format the user has choose we need different amount
					// of space. We therefore create a typical string for the choosen format
					// and determine the length of that string.
					switch ($this->scale->minute->iStyle) {
						case HOURSTYLE_CUSTOM:
							$txt2 = date($this->scale->minute->iLabelFormStr, strtotime('2005-05-15 18:55'));

							break;
						case MINUTESTYLE_MM:
						default:
							$txt2 = '15';

							break;
					}

					$mfw = $this->scale->minute->GetStrWidth($this->img, $txt2) + 6;
					$n2  = ceil(60 / $this->scale->minute->GetIntervall());
					$mw  = $n2 * $mfw;
				}
				$hfw = $hfw < $mw ? $mw : $hfw;
				$n   = ceil(24 * 60 / $this->scale->TimeToMinutes($this->scale->hour->GetIntervall()));
				$hw  = $n * $hfw;
				$fw  = $fw < $hw ? $hw : $fw;
			}

			// We need to repeat this code block here as well.
			// THIS iS NOT A MISTAKE !
			// We really need it since we need to adjust for minutes both in the case
			// where hour scale is shown and when it is not shown.

			if ($this->scale->IsDisplayMinute()) {
				// Depending on what format the user has choose we need different amount
				// of space. We therefore create a typical string for the choosen format
				// and determine the length of that string.
				switch ($this->scale->minute->iStyle) {
					case HOURSTYLE_CUSTOM:
						$txt = date($this->scale->minute->iLabelFormStr, strtotime('2005-05-15 18:55'));

						break;
					case MINUTESTYLE_MM:
					default:
						$txt = '15';

						break;
				}

				$mfw = $this->scale->minute->GetStrWidth($this->img, $txt) + 6;
				$n   = ceil(60 / $this->scale->TimeToMinutes($this->scale->minute->GetIntervall()));
				$mw  = $n * $mfw;
				$fw  = $fw < $mw ? $mw : $fw;
			}

			// If we display week we must make sure that 7*$fw is enough
			// to fit up to 10 characters of the week font (if the week is enabled)
			if ($this->scale->IsDisplayWeek()) {
				// Depending on what format the user has choose we need different amount
				// of space
				$fsw = strlen($this->scale->week->iLabelFormStr);
				if ($this->scale->week->iStyle == WEEKSTYLE_FIRSTDAY2WNBR) {
					$fsw += 8;
				} elseif ($this->scale->week->iStyle == WEEKSTYLE_FIRSTDAYWNBR) {
					$fsw += 7;
				} else {
					$fsw += 4;
				}

				$ww = $fsw * $this->scale->week->GetFontWidth($this->img);
				if (7 * $fw < $ww) {
					$fw = ceil($ww / 7);
				}
			}

			if (!$this->scale->IsDisplayDay() && !$this->scale->IsDisplayHour() &&
				!(($this->scale->week->iStyle == WEEKSTYLE_FIRSTDAYWNBR ||
					$this->scale->week->iStyle == WEEKSTYLE_FIRSTDAY2WNBR) && $this->scale->IsDisplayWeek())) {
				// If we don't display the individual days we can shrink the
				// scale a little bit. This is a little bit pragmatic at the
				// moment and should be re-written to take into account
				// a) What scales exactly are shown and
				// b) what format do they use so we know how wide we need to
				// make each scale text space at minimum.
				$fw /= 2;
				if (!$this->scale->IsDisplayWeek()) {
					$fw /= 1.8;
				}
			}

			$cw = $this->GetMaxActInfoColWidth();
			$this->scale->actinfo->SetMinColWidth($cw);
			if ($this->img->width <= 0) {
				// Now determine the width for the activity titles column

				// Firdst find out the maximum width of each object column
				$titlewidth = max(
					max(
						$this->GetMaxLabelWidth(),
						$this->scale->tableTitle->GetWidth($this->img)
					),
					$this->scale->actinfo->GetWidth($this->img)
				);

				// Add the width of the vertivcal divider line
				$titlewidth += $this->scale->divider->iWeight * 2;

				// Adjust the width by the user specified zoom factor
				$fw *= $this->iZoomFactor;

				// Now get the total width taking
				// titlewidth, left and rigt margin, dayfont size
				// into account
				$width = $titlewidth + $nd * $fw + $lm + $rm;
			} else {
				$width = $this->img->width;
			}

			$width  = round($width);
			$height = round($height);
			// Make a sanity check on image size
			if ($width > MAX_GANTTIMG_SIZE_W || $height > MAX_GANTTIMG_SIZE_H) {
				Util\JpGraphError::RaiseL(6007, $width, $height);
				//("Sanity check for automatic Gantt chart size failed. Either the width (=$width) or height (=$height) is larger than MAX_GANTTIMG_SIZE. This could potentially be caused by a wrong date in one of the activities.");
			}
			$this->img->CreateImgCanvas($width, $height);
			$this->img->SetMargin($lm, $rm, $tm, $bm);
		}
	}

	// Return an array width the maximum width for each activity
	// column. This is used when we autosize the columns where we need
	// to find out the maximum width of each column. In order to do that we
	// must walk through all the objects, sigh...
	public function GetMaxActInfoColWidth(){
		$n = safe_count($this->iObj);
		if ($n == 0) {
			return;
		}

		$w = [];
		$m = $this->scale->actinfo->iLeftColMargin + $this->scale->actinfo->iRightColMargin;

		for ($i = 0; $i < $n; ++$i) {
			$tmp = $this->iObj[$i]->title->GetColWidth($this->img, $m);
			$nn  = safe_count($tmp);
			for ($j = 0; $j < $nn; ++$j) {
				if (empty($w[$j])) {
					$w[$j] = $tmp[$j];
				} else {
					$w[$j] = max($w[$j], $tmp[$j]);
				}
			}
		}

		return $w;
	}

	// Stroke the gantt chart
	public function Stroke($aStrokeFileName = ''){
		// If the filename is the predefined value = '_csim_special_'
		// we assume that the call to stroke only needs to do enough
		// to correctly generate the CSIM maps.
		// We use this variable to skip things we don't strictly need
		// to do to generate the image map to improve performance
		// a best we can. Therefor you will see a lot of tests !$_csim in the
		// code below.
		$_csim = ($aStrokeFileName === _CSIM_SPECIALFILE);

		// Should we autoscale dates?

		if (!$this->scale->IsRangeSet()) {
			list($min, $max) = $this->GetBarMinMax();
			$this->scale->SetRange($min, $max);
		}

		$this->scale->AdjustStartEndDay();

		// Check if we should autoscale the image
		$this->AutoSize();

		// Should we start from the top or just spread the bars out even over the
		// available height
		$this->scale->SetVertLayout($this->iLayout);
		if ($this->iLayout == GANTT_FROMTOP) {
			$maxheight = max($this->GetMaxLabelHeight(), $this->GetMaxBarAbsHeight());
			$this->scale->SetVertSpacing($maxheight * (1 + $this->iLabelVMarginFactor));
		}
		// If it hasn't been set find out the maximum line number
		if ($this->scale->iVertLines == -1) {
			$this->scale->iVertLines = $this->GetBarMaxLineNumber() + 1;
		}

		$maxwidth = max(
			$this->scale->actinfo->GetWidth($this->img),
			max(
				$this->GetMaxLabelWidth(),
				$this->scale->tableTitle->GetWidth($this->img)
			)
		);

		$this->scale->SetLabelWidth($maxwidth + $this->scale->divider->iWeight); //*(1+$this->iLabelHMarginFactor));

		if (!$_csim) {
			$this->StrokePlotArea();
			if ($this->iIconDepth == DEPTH_BACK) {
				$this->StrokeIcons();
			}
		}

		$this->scale->Stroke();

		if (!$_csim) {
			// Due to a minor off by 1 bug we need to temporarily adjust the margin
			--$this->img->right_margin;
			$this->StrokePlotBox();
			++$this->img->right_margin;
		}

		// Stroke Grid line
		$this->hgrid->Stroke($this->img, $this->scale);

		$n = safe_count($this->iObj);
		for ($i = 0; $i < $n; ++$i) {
			//$this->iObj[$i]->SetLabelLeftMargin(round($maxwidth*$this->iLabelHMarginFactor/2));
			$this->iObj[$i]->Stroke($this->img, $this->scale);
		}

		$this->StrokeTitles();

		if (!$_csim) {
			$this->StrokeConstrains();
			$this->footer->Stroke($this->img);

			if ($this->iIconDepth == DEPTH_FRONT) {
				$this->StrokeIcons();
			}

			// Stroke all added user texts
			$this->StrokeTexts();

			// Should we do any final image transformation
			if ($this->iImgTrans) {
				$tform		  = new Image\ImgTrans($this->img->img);
				$this->img->img = $tform->Skew3D(
					$this->iImgTransHorizon,
					$this->iImgTransSkewDist,
					$this->iImgTransDirection,
					$this->iImgTransHighQ,
					$this->iImgTransMinSize,
					$this->iImgTransFillColor,
					$this->iImgTransBorder
				);
			}

			// If the filename is given as the special "__handle"
			// then the image handler is returned and the image is NOT
			// streamed back
			if ($aStrokeFileName == _IMG_HANDLER) {
				return $this->img->img;
			}
			// Finally stream the generated picture
			$this->cache->PutAndStream(
				$this->img,
				$this->cache_name,
				$this->inline,
				$aStrokeFileName
			);
		}
	}

	public function StrokeConstrains(){
		$n = safe_count($this->iObj);

		// Stroke all constrains
		for ($i = 0; $i < $n; ++$i) {
			// Some gantt objects may not have constraints associated with them
			// for example we can add Plot\IconPlots which doesn't have this property.
			if (empty($this->iObj[$i]->constraints)) {
				continue;
			}

			$numConstrains = safe_count($this->iObj[$i]->constraints);

			for ($k = 0; $k < $numConstrains; ++$k) {
				$vpos = $this->iObj[$i]->constraints[$k]->iConstrainRow;
				if ($vpos >= 0) {
					$c1 = $this->iObj[$i]->iConstrainPos;

					// Find out which object is on the target row
					$targetobj = -1;
					for ($j = 0; $j < $n && $targetobj == -1; ++$j) {
						if ($this->iObj[$j]->iVPos == $vpos) {
							$targetobj = $j;
						}
					}
					if ($targetobj == -1) {
						Util\JpGraphError::RaiseL(6008, $this->iObj[$i]->iVPos, $vpos);
						//('You have specifed a constrain from row='.$this->iObj[$i]->iVPos.' to row='.$vpos.' which does not have any activity.');
					}
					$c2 = $this->iObj[$targetobj]->iConstrainPos;
					if (safe_count($c1) == 4 && safe_count($c2) == 4) {
						switch ($this->iObj[$i]->constraints[$k]->iConstrainType) {
							case CONSTRAIN_ENDSTART:
								if ($c1[1] < $c2[1]) {
									$link = new Image\GanttLink($c1[2], $c1[3], $c2[0], $c2[1]);
								} else {
									$link = new Image\GanttLink($c1[2], $c1[1], $c2[0], $c2[3]);
								}
								$link->SetPath(3);

								break;
							case CONSTRAIN_STARTEND:
								if ($c1[1] < $c2[1]) {
									$link = new Image\GanttLink($c1[0], $c1[3], $c2[2], $c2[1]);
								} else {
									$link = new Image\GanttLink($c1[0], $c1[1], $c2[2], $c2[3]);
								}
								$link->SetPath(0);

								break;
							case CONSTRAIN_ENDEND:
								if ($c1[1] < $c2[1]) {
									$link = new Image\GanttLink($c1[2], $c1[3], $c2[2], $c2[1]);
								} else {
									$link = new Image\GanttLink($c1[2], $c1[1], $c2[2], $c2[3]);
								}
								$link->SetPath(1);

								break;
							case CONSTRAIN_STARTSTART:
								if ($c1[1] < $c2[1]) {
									$link = new Image\GanttLink($c1[0], $c1[3], $c2[0], $c2[1]);
								} else {
									$link = new Image\GanttLink($c1[0], $c1[1], $c2[0], $c2[3]);
								}
								$link->SetPath(3);

								break;
							default:
								Util\JpGraphError::RaiseL(6009, $this->iObj[$i]->iVPos, $vpos);
								//('Unknown constrain type specified from row='.$this->iObj[$i]->iVPos.' to row='.$vpos);
								break;
						}

						$link->SetColor($this->iObj[$i]->constraints[$k]->iConstrainColor);
						$link->SetArrow(
							$this->iObj[$i]->constraints[$k]->iConstrainArrowSize,
							$this->iObj[$i]->constraints[$k]->iConstrainArrowType
						);

						$link->Stroke($this->img);
					}
				}
			}
		}
	}

	public function GetCSIMAreas(){
		if (!$this->iHasStroked) {
			$this->Stroke(_CSIM_SPECIALFILE);
		}

		$csim = $this->title->GetCSIMAreas();
		$csim .= $this->subtitle->GetCSIMAreas();
		$csim .= $this->subsubtitle->GetCSIMAreas();

		$n = safe_count($this->iObj);
		for ($i = $n - 1; $i >= 0; --$i) {
			$csim .= $this->iObj[$i]->GetCSIMArea();
		}

		return $csim;
	}
}
